<?php
namespace Easy;

use Easy\Daemon\Ini;

class Daemon
{

    const PID_FILE = 'daemon/asyncd.pid';
    const INI_FILE = 'daemon/asyncd.ini';
    const LOG_TAG  = 'daemon';

    private static $_instance = null;
    private        $_pids     = array();
    private        $_config   = array();


    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    public function init()
    {
        self::_checkPidFile();
        $pid = self::_createDaemon();
        self::_savePidFile($pid);
        declare(ticks = 1);
        $this->initSignal();
        $this->_getConfig();
        $this->_createWorkers();
    }

    private static function out($msg)
    {
        fwrite(STDOUT, $msg . PHP_EOL);
        fflush(STDOUT);
    }

    private static function pidFile()
    {
        return APPLICATION_ROOT . '/' . self::PID_FILE;
    }

    private static function iniFile()
    {
        return APPLICATION_ROOT . '/' . self::INI_FILE;
    }

    private static function _checkPidFile()
    {
        $pidFile = self::pidFile();
        if (file_exists($pidFile)) {
            $pid = trim(file_get_contents($pidFile));
            if (posix_kill($pid, 0)) {
                throw new EasyException('Server alraady running with PID ' . $pid, self::LOG_TAG);
            }
            if ( ! unlink($pidFile)) {
                throw new EasyException('Cannot unlink PID file ' . $pid, self::LOG_TAG);
            }
        }
    }

    private static function _createDaemon()
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new EasyException('Failed to fork', self::LOG_TAG);
        }
        if ($pid) {
            exit(0);
        }
        posix_setsid();
        chdir('/');
        umask(0);
        $daemonPid = posix_getpid();
        $str       = 'Start daemon process with pid ' . $daemonPid;
        self::out($str);
        EasyLog::log($str, self::LOG_TAG, EasyLog::MSG);

        return $daemonPid;
    }

    private static function _savePidFile($pid)
    {
        $pidFile = self::pidFile();
        file_put_contents($pidFile, $pid);
    }

    public function initSignal()
    {
        pcntl_signal(SIGHUP, array(&$this, 'sigHandler'), false);
        pcntl_signal(SIGINT, array(&$this, 'sigHandler'), false);
        pcntl_signal(SIGTERM, array(&$this, 'sigHandler'), false);
        pcntl_signal(SIGUSR1, array(&$this, 'sigHandler'), false);
    }

    public function sigHandler($signo)
    {
        switch ($signo) {
            case SIGTERM:
            case SIGINT:
            case SIGHUP:
            case SIGQUIT:
                foreach ($this->_pids as $pid) {
                    posix_kill($pid, $signo);
                }
                foreach ($this->_pids as $pid) {
                    $status = null;
                    pcntl_waitpid($pid, $status);
                }
                $str = 'Asyncd pid ' . getmypid() . ' exiting.';
                self::out($str);
                EasyLog::log($str, self::LOG_TAG, EasyLog::MSG);
                exit();
            case SIGUSR1:
                $str = getmypid() . ' Total children : ' . sizeof($this->_pids);
                self::out($str);
                EasyLog::log($str, self::LOG_TAG, EasyLog::MSG);
                break;
        }
    }

    private function _createWorkers()
    {
        while (1) {
            $this->_runWorker();
            $this->_waitWorker();
            usleep(5000);
        }
    }

    private function _getConfig()
    {
        $this->_config = Ini::parse(self::iniFile());
        if ( ! $this->_config) {
            $str = 'Can not find the child module in the asyncd.ini file';
            EasyLog::log($str, self::LOG_TAG, EasyLog::MSG);
        }
    }

    private function _runWorker()
    {
        foreach ($this->_config as $module => $subModules) {
            foreach ($subModules as $subModule => $config) {
                $class  = $module;
                $method = $subModule;
                if (array_key_exists('total_threads', $config)) {
                    $maxThread = $config['total_threads'];
                } else {
                    $maxThread = $config['total_task'] / $config['task_per_thread'];
                }
                if ( ! isset($config['enabled']) || ! $config['enabled']) {
                    continue;
                }
                if ( ! method_exists($class, $method)) {
                    throw new EasyException($method . ' not exist in ' . $class);
                }
                for ($i = 0; $i < $maxThread; $i++) {
                    $key = $class."_".$method.$i;
                    if (array_key_exists($key, $this->_pids)) {
                        continue;
                    }
                    $pid = pcntl_fork();
                    if ( ! $pid) {
                        $str = 'Start class ' . $class . ' in child process ' . getmypid();
                        EasyLog::log($str, self::LOG_TAG, EasyLog::MSG);
                        new $class($method, $i, $config);
                        exit();
                    } else {
                        $this->_pids[$key] = $pid;
                    }
                }
            }
        }
    }

    private function _waitWorker()
    {
        $deadPid = pcntl_waitpid(-1, $status, WNOHANG);
        while ($deadPid > 0) {
            // Remove the dead pid from the array
            unset($this->_pids[array_search($deadPid, $this->_pids)]);
            // Look for another one
            $deadPid = pcntl_waitpid(-1, $status, WNOHANG);
        }
    }
}
